using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using UnityEngine.SceneManagement;
using UnityEditor;
#if !UNITY_EDITOR && UNITY_ANDROID
using ICSharpCode.SharpZipLib.Zip;
#endif

namespace XLib
{
    public class Assets : MonoBehaviour
    {
        [RuntimeInitializeOnLoadMethod]
        static void PlayReset()
        {
            isInitialize = false;
            cacheBundleRequest.Clear();
            loadingBundleRequest.Clear();
            waitLoadBundleRequest.Clear();
            unUseBundleRequest.Clear();
            cacheAssetRequest.Clear();
            loadingAssetRequest.Clear();
            waitLoadAssetRequest.Clear();
            unUseAssetRequest.Clear();
            cacheSceneRequest.Clear();
            loadingSceneRequest.Clear();
            waitLoadSceneRequest.Clear();
            unUseSceneRequest.Clear();
            manifest = null;
        }

#if UNITY_EDITOR
        public static bool IsBundleMode = false;
#endif
        private static Dictionary<string, BundleRequest> cacheBundleRequest = new Dictionary<string, BundleRequest>();
        private static List<BundleRequest> loadingBundleRequest = new List<BundleRequest>();
        private static List<BundleRequest> waitLoadBundleRequest = new List<BundleRequest>();
        private static List<string> unUseBundleRequest = new List<string>();
        private static Dictionary<string, AssetRequest> cacheAssetRequest = new Dictionary<string, AssetRequest>();
        private static List<AssetRequest> loadingAssetRequest = new List<AssetRequest>();
        private static List<AssetRequest> waitLoadAssetRequest = new List<AssetRequest>();
        private static List<string> unUseAssetRequest = new List<string>();
        private static Dictionary<string, SceneRequest> cacheSceneRequest = new Dictionary<string, SceneRequest>();
        private static List<SceneRequest> loadingSceneRequest = new List<SceneRequest>();
        private static List<SceneRequest> waitLoadSceneRequest = new List<SceneRequest>();
        private static List<string> unUseSceneRequest = new List<string>();

        public static Manifest manifest { get; private set;}
        public static readonly string Extension = "unity3d";
        public static readonly string version = "version.json";
        private static readonly string MainfestAsset = "manifest" + "." + Extension;
        private static readonly int MAX_LOAD_BUNDLE_NUM = 5;

        public static Func<byte, long, byte> Decryptor { get; set; }

#if !UNITY_EDITOR && UNITY_ANDROID
        private static ZipFile apkZip;
#endif

        private static string _datePath;
        public static string dataPath {
            get {
                if (string.IsNullOrEmpty(_datePath))
                    _datePath = Path.Combine(Application.streamingAssetsPath, GameUtility.GetPlatformName());
                return _datePath;
            }
        }

        private static string _updatePath;
        public static string updatePath {
            get {
                if (string.IsNullOrEmpty(_updatePath))
                {
                    _updatePath = Path.Combine(Application.persistentDataPath, "DLC");
                }
                return _updatePath;
            }
        }

        private HashSet<AssetRequest> assetRequestLiveInScene;

        public static bool isInitialize { get; private set;}

        public static IEnumerator Initialize()
        {
            if (isInitialize)
                yield break;

            Assets assets = GameObject.FindObjectOfType<Assets>();
            if (null == assets)
            {
                GameObject go = new GameObject("Assets", new Type[] { typeof(Assets) });
                DontDestroyOnLoad(go);
            }

            manifest = Manifest.LoadApkManifest(MainfestAsset);
            manifest?.Initialize();

            GameUtility.CreateFolder(updatePath);

            isInitialize = true;
        }

        public static string GetPriorityPath(string path)
        {
            string priorityPath = Path.Combine(updatePath, path);

            if (File.Exists(priorityPath))
            {
                return priorityPath;
            }

            priorityPath = Path.Combine(dataPath, path);

            return priorityPath;
        }

        public static void UpdateManifest(Manifest newManifest)
        {
            manifest = newManifest;
        }

        internal static string[] GetAssetBundleDepends(string bundleName)
        {
            return manifest.GetBundleDepends(bundleName);
        }

        internal static string GetBundleHashName(string bundleName)
        {
            return manifest.GetAssetBundle(bundleName);
        }

        public static bool ExistBundle(string bundleName)
        {
            return manifest.ExistBundle(bundleName) || MainfestAsset.Equals(bundleName);
        }

        internal static BundleRequest LoadBundleAsync(string bundleName)
        {
            return LoadBundle(bundleName, true);
        }

        internal static BundleRequest LoadBundle(string bundleName)
        {
            return LoadBundle(bundleName, false);
        }

        private static BundleRequest LoadBundle(string bundleName, bool bAsync)
        {
            BundleRequest request;
            if (!cacheBundleRequest.TryGetValue(bundleName, out request))
            {
                request = bAsync ? new BundleRequestAsync(bundleName) : new BundleRequest(bundleName);
                cacheBundleRequest.Add(request.name, request);
            }

            request.Retain();
            if (bAsync)
                waitLoadBundleRequest.Add(request);
            else
                request.Load();

            return request;
        }

        public static AssetRequest LoadAssetAsync(string assetPath, Type type)
        {
            return LoadAsset(assetPath, type, true);
        }

        public static AssetRequest LoadAsset(string assetPath, Type type)
        {
            return LoadAsset(assetPath, type, false);
        }

        public static void UnLoadAsset(AssetRequest assetRequest)
        {
            assetRequest?.Release();
        }

        private static AssetRequest LoadAsset(string assetPath, Type type, bool bAsync)
        {
            AssetRequest request;
            if (!cacheAssetRequest.TryGetValue(assetPath, out request))
            {
                if (assetPath.StartsWith("http://")
                    || assetPath.StartsWith("https://"))
                    request = new WebRequestAsync(assetPath, type);
                else
                    request = bAsync ? new AssetRequestAsync(assetPath, type) : new AssetRequest(assetPath, type);
                cacheAssetRequest.Add(request.assetPath, request);
            }

            request.Retain();
            if (bAsync)
                waitLoadAssetRequest.Add(request);
            else
                request.Load();

            return request;
        }

        public void KeepAliveInScene(AssetRequest assetRequest)
        {
            if (!assetRequestLiveInScene.Contains(assetRequest))
            {
                assetRequest.Retain();
                assetRequestLiveInScene.Add(assetRequest);
            }
        }

        public void ClearAliveInScene(AssetRequest assetRequest)
        {
            if (assetRequestLiveInScene.Contains(assetRequest))
            {
                assetRequest.Release();
                assetRequestLiveInScene.Remove(assetRequest);
            }
        }

        public static SceneRequest LoadSceneAsync(string assetPath, LoadSceneMode loadSceneMode)
        {
            return LoadScene(assetPath, loadSceneMode, true);
        }

        public static SceneRequest LoadScene(string assetPath, LoadSceneMode loadSceneMode)
        {
            return LoadScene(assetPath, loadSceneMode, false);
        }

        private static SceneRequest LoadScene(string assetPath, LoadSceneMode loadSceneMode, bool bAsync)
        {
            SceneRequest request;
            if (!cacheSceneRequest.TryGetValue(assetPath, out request))
            {
                request = bAsync ? new SceneRequestAsync(assetPath, loadSceneMode) : new SceneRequest(assetPath, loadSceneMode);
                cacheSceneRequest.Add(assetPath, request);
            }

            request.Retain();
            if (bAsync)
                waitLoadSceneRequest.Add(request);
            else
                request.Load();

            return request;
        }

        public static void UnloadScene(SceneRequest sceneRequest)
        {
            sceneRequest.Release();
        }

        internal static Stream GetPriorityStream(string relativePath)
        {
            Stream stream = null;
            var path = Path.Combine(updatePath, relativePath);
            if (File.Exists(path))
                stream = new FileStream(path, FileMode.Open, FileAccess.Read);
            else
                stream = LoadStreamInStreamingAsset(Path.Combine(GameUtility.GetPlatformName(), relativePath));
            
            stream = new DecryptStream(stream, CryptoSettings.Decrypt);

            return stream;
        }

        internal static AssetBundle LoadBundleFromFile(string relativePath)
        {
            if (CryptoSettings.Enable)
            {
                var bundleStream = GetPriorityStream(relativePath);
                return AssetBundle.LoadFromStream(bundleStream);
            }

            var path = GetPriorityPath(relativePath);
            return AssetBundle.LoadFromFile(path);
        }

        internal static AssetBundleCreateRequest LoadBundleFromFileAsync(string relativePath)
        {
            if (CryptoSettings.Enable)
            {
                var bundleStream = GetPriorityStream(relativePath);
                return AssetBundle.LoadFromStreamAsync(bundleStream);
            }

            var path = GetPriorityPath(relativePath);
            return AssetBundle.LoadFromFileAsync(path);
        }

        public static bool FileExist(string relativePath)
        {
            var fullPath = Path.Combine(Application.persistentDataPath, relativePath);
            if (File.Exists(fullPath))
                return true;

            return ExistInStreamingAsset(relativePath);
        }

        public static string ReadFileText(string relativePath)
        {
            var bytes = ReadFileBytes(relativePath);
            if (bytes != null)
                return System.Text.Encoding.UTF8.GetString(bytes);
            return null;
        }

        public static byte[] ReadFileBytes(string relativePath)
        {
            byte[] bytes = null;
            var stream = ReadFileStream(relativePath);
            if (stream != null)
            {
                MemoryStream ms = new MemoryStream();
                stream.CopyTo(ms);
                bytes = ms.ToArray();
                ms.Dispose();
                stream.Dispose();
            }
            return bytes;
        }

        public static Stream ReadFileStream(string relativePath)
        {
            var fullPath = Path.Combine(Application.persistentDataPath, relativePath);
            if (File.Exists(fullPath))
                return new FileStream(fullPath, FileMode.Open, FileAccess.Read);

            return LoadStreamInStreamingAsset(relativePath);
        }

        public static bool ExistInStreamingAsset(string relativePath)
        {
            var fullPath = relativePath;
#if !UNITY_EDITOR && UNITY_IOS
            fullpath = string.Format("{0}/Raw/{1}", Application.dataPath, relativePath);
            return File.Exists(fullPath);
#elif !UNITY_EDITOR && UNITY_ANDROID
            if (apkZip == null)
                apkZip = new ZipFile(Application.dataPath, new StringCodec() { CodePage = System.Text.Encoding.UTF8.CodePage });

            var index = apkZip.FindEntry("assets/" + relativePath, false);
            return index >= 0;
#else
            fullPath = Path.Combine(Application.streamingAssetsPath, relativePath);
            return File.Exists(fullPath);
#endif
        }

        public static string LoadContentInStreamingAsset(string relativePath)
        {
            var bytes = LoadBytesInStreamingAsset(relativePath);
            if (bytes != null)
                return System.Text.Encoding.UTF8.GetString(bytes);
            return null;
        }

        public static byte[] LoadBytesInStreamingAsset(string relativePath)
        {
            byte[] bytes = null;
            var stream = LoadStreamInStreamingAsset(relativePath);
            if (stream != null)
            {
                var mem = new MemoryStream();
                stream.CopyTo(mem);
                bytes = mem.ToArray();
                mem.Dispose();
                stream.Dispose();
            }
            return bytes;
        }

        public static Stream LoadStreamInStreamingAsset(string relativePath)
        {
            string fullPath = relativePath;
            Stream stream = null;

#if !UNITY_EDITOR && UNITY_IOS
            fullpath = string.Format("{0}/Raw/{1}", Application.dataPath, relativePath);
            if (File.Exists(fullPath))
                stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read);
#elif !UNITY_EDITOR && UNITY_ANDROID
            if (apkZip == null)
                apkZip = new ZipFile(Application.dataPath, new StringCodec() { CodePage = System.Text.Encoding.UTF8.CodePage });

            var entry = apkZip.GetEntry("assets/" + relativePath);
            if (entry != null)
                stream = apkZip.GetInputStream(entry);
            stream = new ApkZipStream(stream);
#else
            fullPath = Path.Combine(Application.streamingAssetsPath, relativePath);
            if (File.Exists(fullPath))
                stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read);
#endif

            if (stream == null)
                Debug.LogError(string.Format("Can not find {0} in LoadStreamInRaw!", fullPath));

            return stream;
        }

        private void Update()
        {
            // bundle load
            foreach (var item in cacheBundleRequest)
            {
                var bundleRequest = item.Value;
                if (bundleRequest.IsUnUse())
                {
                    unUseBundleRequest.Add(item.Key);
                    waitLoadBundleRequest.Remove(bundleRequest);
                    loadingBundleRequest.Remove(bundleRequest);
                    bundleRequest.UnLoad();
                }
            }

            foreach (var bundleKey in unUseBundleRequest)
            {
                cacheBundleRequest.Remove(bundleKey);
            }
            unUseBundleRequest.Clear();

            for (int i = 0; i < waitLoadBundleRequest.Count; i++)
            {
                if (loadingBundleRequest.Count >= MAX_LOAD_BUNDLE_NUM) break;

                BundleRequest bundleRequest = waitLoadBundleRequest[i];
                bundleRequest.Load();
                loadingBundleRequest.Add(bundleRequest);
                waitLoadBundleRequest.RemoveAt(i);
                i--;
            }

            for (int i = 0; i < loadingBundleRequest.Count; i++)
            {
                BundleRequest bundleRequest = loadingBundleRequest[i];
                if (!bundleRequest.Update())
                {
                    loadingBundleRequest.RemoveAt(i);
                    i--;
                }
            }

            // asset load
            foreach (var item in cacheAssetRequest)
            {
                var assetRequest = item.Value;
                if (assetRequest.IsUnUse())
                {
                    unUseAssetRequest.Add(item.Key);
                    waitLoadAssetRequest.Remove(assetRequest);
                    loadingAssetRequest.Remove(assetRequest);
                    assetRequest.UnLoad();
                }
            }

            foreach (var assetKey in unUseAssetRequest)
            {
                cacheAssetRequest.Remove(assetKey);
            }
            unUseAssetRequest.Clear();

            for (int i = 0; i < waitLoadAssetRequest.Count; i++)
            {
                AssetRequest assetRequest = waitLoadAssetRequest[i];
                assetRequest.Load();
                loadingAssetRequest.Add(assetRequest);
                waitLoadAssetRequest.RemoveAt(i);
                i--;
            }

            for (int i = 0; i < loadingAssetRequest.Count; i++)
            {
                AssetRequest assetRequest = loadingAssetRequest[i];
                if (!assetRequest.Update())
                {
                    loadingAssetRequest.RemoveAt(i);
                    i--;
                }
            }

            // scene load
            foreach (var item in cacheSceneRequest)
            {
                var sceneRequest = item.Value;
                if (sceneRequest.IsUnUse())
                {
                    unUseSceneRequest.Add(item.Key);
                    waitLoadSceneRequest.Remove(sceneRequest);
                    loadingSceneRequest.Remove(sceneRequest);
                    sceneRequest.UnLoad();
                }
            }

            foreach (var assetKey in unUseSceneRequest)
            {
                cacheSceneRequest.Remove(assetKey);
            }
            unUseSceneRequest.Clear();

            for (int i = 0; i < waitLoadSceneRequest.Count; i++)
            {
                SceneRequest sceneRequest = waitLoadSceneRequest[i];
                sceneRequest.Load();
                loadingSceneRequest.Add(sceneRequest);
                waitLoadSceneRequest.RemoveAt(i);
                i--;
            }

            for (int i = 0; i < loadingSceneRequest.Count; i++)
            {
                SceneRequest scenenRequest = loadingSceneRequest[i];
                if (!scenenRequest.Update())
                {
                    loadingSceneRequest.RemoveAt(i);
                    i--;
                }
            }
        }
    }
}